package jsecurity.cert;

import java.math.BigInteger;
import java.security.KeyException;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Date;

import javax.security.auth.x500.X500Principal;

import jsecurity.JSecurity;
import jsecurity.asym.KeyPair;
import jsecurity.exceptions.CertificateCreationException;

import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509v1CertificateBuilder;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;

/**
 * This class represents a X.509 public key certificate based on the elliptic curve key-generation algorithm
 * 
 * @author João Dias Amaro
 */
public class X509Certificate implements PublicKeyCertificate
{
	static
	{
		JSecurity.init();
	}
	
	private X509CertificateHolder certificateHolder;
	
	/**
	 * Creates an Elliptic Curved X.509 Certificate signed by a given Issuer. If the public key to sign is the pair of the issuer's private key then
	 * calling this constructor generates a self-signed certificate
	 * 
	 * @param subjectPublicKey the Public Key to be signed by this certificate
	 * @param issuerPrivateKey the Private Key of the issuer of this certificate (eg. the Certificate Authority Private Key)
	 * @param subjectDN the Distinguished Name of the subject of owner of this certificate
	 * @param issuerDN the Distinguished Name of the issuer of owner of this certificate
	 * @param serialNumber the serial number of this certificate
	 * @param start the starting date of the validity period of this certificate
	 * @param end the ending date of the validity period of this certificate
	 * @throws CertificateCreationException if the certificate could not be created. This exception has a nested exception with the cause
	 */
	public X509Certificate(PublicKey subjectPublicKey, PrivateKey issuerPrivateKey, String subjectDN, String issuerDN, BigInteger serialNumber, Date start, Date end) throws CertificateCreationException
	{
		if(subjectPublicKey == null || issuerPrivateKey == null) throw new CertificateCreationException("The certificate keys must be defined", new NullPointerException());
		if(subjectDN == null || issuerDN == null) throw new CertificateCreationException("The Distinguished Names must be defined", new NullPointerException());
		if(serialNumber == null) throw new CertificateCreationException("The Serial Number must be defined", new NullPointerException());
		if(start == null || end == null) throw new CertificateCreationException("The validity dates must be defined", new NullPointerException());
		if(start.after(end)) throw new CertificateCreationException("The start date must occur earlier than the end date", new IllegalArgumentException());
		
		X500Principal subjectPrincipal = new X500Principal(subjectDN);
		X500Principal issuerPrincipal = new X500Principal(issuerDN);
		JcaX509v1CertificateBuilder builder = new JcaX509v1CertificateBuilder(subjectPrincipal, serialNumber, start, end, issuerPrincipal, subjectPublicKey);
		
		try 
		{
			this.certificateHolder = builder.build(new JcaContentSignerBuilder("SHA512withRSA").setProvider("BC").build(issuerPrivateKey));
		} 
		catch(OperatorCreationException e) 
		{
			throw new CertificateCreationException("It was not possible to create the elliptic curve certificate", e);
		}
	}
	
	/**
	 * Creates a self-signed Elliptic Curve X.509 Certificate
	 * 
	 * @param subjectKeyPair the Key pair containing the public and private keys to create this certificate
	 * @param subjectDN the distinguished name of the owner of the certificate
	 * @param serialNumber the serial number of the certificate
	 * @param start the starting date of the validity period of this certificate
	 * @param end the ending date of the validity period of this certificate
	 * @throws CertificateCreationException if the certificate could not be created. This exception has a nested exception with the cause
	 */
	public X509Certificate(KeyPair subjectKeyPair, String subjectDN, BigInteger serialNumber, Date start, Date end) throws CertificateCreationException
	{
		this(subjectKeyPair.getPublicKey(), subjectKeyPair.getPrivateKey(), subjectDN, subjectDN, serialNumber, start, end);
	}
	
	/**
	 * Creates a self-signed certificate and defaults the parameters that are not in the arguments
	 * 
	 * @param keypair the Key pair containing the public and private keys to create this certificate
	 * @param subjectDN the distinguished name of the owner of the certificate
	 * @param validityDays the period, in days, in which this certificate will be valid, starting today
	 * @throws CertificateCreationException if the certificate could not be created. This exception has a nested exception with the cause
	 */
	public X509Certificate(KeyPair keypair, String subjectDN, int validityDays) throws CertificateCreationException
	{
		this(keypair, subjectDN, BigInteger.valueOf(System.currentTimeMillis()), new Date(), new Date(new Date().getTime() + (1000L * 60 * 60 * 24 * validityDays)));
	}
	
	/**
	 * Creates a self-signed certificate based only in the supplied Key pair. Defaults all the other certificate parameters
	 * 
	 * @param keypair the Key pair containing the public and private keys to create this certificate
	 * @throws CertificateCreationException if the certificate could not be created. This exception has a nested exception with the cause
	 */
	public X509Certificate(KeyPair keypair) throws CertificateCreationException
	{
		this(keypair, "CN=XXX", 365);
	}	
	
	/*
	 * Certificate Interface
	 */
	
	@Override
	public PublicKey getPublicKey() throws KeyException 
	{
		try
		{
			byte[] encodedPublicKeySpec = this.certificateHolder.getSubjectPublicKeyInfo().getEncoded();
			
			return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(encodedPublicKeySpec));
		}
		catch(Exception e)
		{
			throw new KeyException("It was not possible to retrive the public key from the certificate", e);
		}
	}

	@Override
	public BigInteger getSerialNumber() 
	{
		return this.certificateHolder.getSerialNumber();
	}

	@Override
	public boolean isValid()
	{
		return this.isValidOn(null);
	}

	@Override
	public boolean isValidOn(Date date)
	{
		Date dateToCheck;
		
		if(date == null) dateToCheck = new Date();
		else dateToCheck = date;
		
		return this.certificateHolder.isValidOn(dateToCheck);
	}

	@Override
	public Boolean isSelfSigned()
	{
		try 
		{
			return this.certificateHolder.isSignatureValid(new JcaContentVerifierProviderBuilder().setProvider("BC").build(this.getPublicKey()));
		} 
		catch(Exception e)
		{
			return null;
		}
	}
}
